Paramify VER fetchers: review fixes, category-level config, and paramify programs - #22
Merged
Conversation
Port the three VER-* vulnerability report generators into the framework:
- fetchers/paramify/{accepted_vulnerabilities,vulnerability_detail_report,historical_ver_activity}
- shared logic in _shared/ver_common.py: one accepted-definition + one fetch/
mapping implementation, so the three reports partition consistently by
construction (AVI accepted / VDT non-accepted / MRH both).
- _categories/paramify.yaml + category README.
Reads from Paramify's own REST API; writes FedRAMP CR2026 report JSON as the
payload (runner adds the envelope). Keeps a vendor _summary; no schema
verdict in the fetcher (Paramify-side). Verified end-to-end against stage via
paramify run: AVI 1 accepted, VDT 2126 non-accepted, MRH 2127 total
(2126 active / 1 accepted); dispositions 652/2/1/1471; 872 overdue,
873 unevaluated. 227 framework tests pass.
project_id and cert_package_uri move from secrets to target_schema: both are per-program properties, and a public CPO URI was never a secret. The runner now invokes each fetcher once per program, and the envelope carries the target. Output filenames gain a sanitized project_id suffix (shared sanitize_for_filename in ver_common). The runner discovers outputs by diffing the evidence dir, so without this the second program would silently overwrite the first and its outputs list would come back empty. Verified against stage with two programs via paramify run (6/6 OK): Wiz (FEDRAMP_REV_5) 1 accepted / 2126 active / 2126 non-accepted, matching prior single-target runs; and a program with zero issues, which produces a conformant report with all required fields present and empty arrays. 227 framework tests pass.
Review of the three FedRAMP VER report fetchers (AVI, VDT, MRH) turned up correctness bugs and a contract mismatch. All fixes are shared-module level, so the AVI/VDT partition stays consistent by construction. Correctness: - A pending or rejected RISK_ADJUSTMENT reported finalDisposition "Partially Mitigated". Mitigation now requires an ACCEPTED deviation — asserting partial mitigation on the strength of a decision nobody has made yet is a compliance misstatement. - map_vulnerability_detail raised KeyError on an issue with neither poamId nor id, killing the whole report. It now emits an empty providerTrackingId, which schema verification can flag per-record. - PARAMIFY_HTTP_TIMEOUT was int()-parsed at import, so a malformed value aborted the run with a bare ValueError before main() could log anything. It resolves at call time and falls back to the default with a warning. - Callers pre-truncated dates to 10 chars, so a timestamped report_to silently over-included up to a day past the declared period. The window helper now decides date-only vs timestamp itself. Contract: - report_from/report_to/api_base_url/http_timeout were declared under secrets[]. Every declared secret is mandatory — the runner raises when a manifest omits one — which made the optional knobs required, contradicting the README. They are config now. - cert_package_uri, api_base_url and http_timeout moved to category config (fetchers/_categories/paramify.yaml). One workspace publishes one certification package URI and talks to one Paramify instance, so these are set once under platforms.paramify.config rather than copied onto every target. Mirrors rippling.yaml, which already declares base_url/page_size at category level. - PARAMIFY_REPORT_TO was read by all three fetchers and documented as optional but declared nowhere, so the runner — which passes only declared env vars — could never set it. - Added an optional program_name target field for readable evidence filenames. Evidence payload: - _summary now carries a `collection` block (status + api_failures). /issues is the only call these fetchers make, so a failure leaves the report arrays empty; the house pattern is to still write the file with the ledger inside it, and without this an empty failed report reads as a genuinely clean one. Also deduplicated the acceptance rationale (2x) and the unevaluated-backlog warning (3x) into _shared/ver_common.py, and promoted the cross-module helpers off underscore-private names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Paramify API identifies programs by project UUID; the product UI and the
people running these fetchers use names. Fanning a fetcher out across a
workspace meant copying UUIDs by hand into targets[].
paramify programs list # readable name + UUID for every program
paramify programs target # select programs, write them as manifest targets
`target` selects interactively (numbers, ranges, or "all"), by --program
NAME|ID, or --all; resolves a selector as an exact id, an exact name, then a
unique case-insensitive substring, and refuses an ambiguous match rather than
guessing. With no fetcher argument it targets every manifest entry whose
target_schema declares project_id, so one command fans out all three VER
reports. Programs already targeted are skipped, so a re-run tops the manifest up
instead of duplicating entries.
Shared values the targeted fetchers need but that don't vary per program
(--cert-uri, --report-from) are asked for once and written to
platforms.<category>.config, where every fetcher in the category picks them up.
Each is prompted for only when genuinely missing — required, no default, and
absent from both the platform block and the entry's own config. --report-from is
validated as an ISO date up front: an unparseable one yields an empty report
window, which silently drops every closed issue rather than failing.
Implementation notes:
- The workspace lookup (GET /projects) is ~50 lines of requests in the api
facade rather than a dependency on paramify-sdk, which pfy uses. The SDK is
pinned by a private git URL and this repo is heading for public release; a git
dependency would break `pip install` for outside users. It matches the
requests-based client the evidence uploader already ships.
- Selection, resolution and manifest wiring live in framework.api so the TUI can
grow the same capability without reimplementing any of it; the CLI only
renders. `target` composes api.add_target(), so it produces exactly the
manifest a hand-written `manifest add-target` would.
- Nothing prompts under --json; every error path emits {ok, path, errors}.
The uploader now prefers a target's program_name over its opaque id when titling
an artifact, so per-program artifacts in one evidence set read as
"… - Alpha Cloud Services" instead of a bare UUID. Fetchers whose id is already
readable (gitlab's group/project) declare no program_name and are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manifest screen read only a fetcher entry's own `config` block, so any value set once at the category level rendered as "required — unset" on every entry that inherited it, and the summary column undercounted. api.validate() got this right all along, which is why `paramify validate` reported a manifest runnable while the TUI showed a missing required field. Pre-existing — it would have misreported rippling's base_url/page_size and checkov's soft_fail the same way. Moving the Paramify VER fetchers' shared knobs to category config is just what made it reachable. Adds api.effective_config(), which performs the same merge the runner does (platform defaults <- platform values <- per-fetcher values) and returns each field with its value plus the layer it came from. Both the detail pane and the count now render that, so they can't disagree with each other or with validate. Batched over every entry so a table redraw scans the fetcher tree once. Two things fall out of resolving it at the facade rather than patching the renderer: fields the *category* declares now appear at all (previously cert_package_uri was invisible on entries it applies to, despite being required and injected into every invocation), and each value shows its provenance, so it's clear which layer to change. The edit form is untouched and stays safe: FieldRow.get_value() returns None for a blank input and the save path skips it, so a blank inherited field means "inherit", not "overwrite with empty". Also records the whole branch under CHANGELOG [Unreleased]. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Need to refactor to reduce LOC and fix time to be UTC |
A generated report mixed three timestamp notations, because it drew on three
sources and only one of them was formatted:
- values the fetcher generates (generatedAt, a defaulted reportPeriod.to) —
already %Y-%m-%dT%H:%M:%SZ
- values from the Paramify API (createdAt -> detectedAt, evaluationDate ->
evaluationCompletedAt, and the dueDate quoted in an overdue explanation) —
passed through verbatim, so millisecond precision landed in the report
- report bounds from config — echoed verbatim, so a bare "2026-01-01" sat next
to a full timestamp in the same reportPeriod object
All three now go through to_utc_z(), so one document carries one notation:
2026-07-30T09:00:00Z
A non-UTC offset is converted rather than preserved (…T09:00:00+02:00 becomes
…T07:00:00Z). A value the parser can't read is passed through unchanged rather
than dropped or blanked — losing a value the source gave us is worse than an
off-format one, and schema verification is where that should surface.
The report bounds are normalized for DISPLAY only; the raw config values still
drive fetch_all_issues. Normalizing before the window is computed would turn a
date-only bound into a midnight instant and silently drop that day's closures.
For the same reason a date-only report_to is reported as that day's last second
(2026-06-30 -> 2026-06-30T23:59:59Z): the filter treats a date-only end as
"through the end of that day", so reporting its midnight would understate the
period by a day in a compliance artifact.
Verified end to end against a stub returning the API's real shapes (milliseconds
and a +02:00 offset): all 60 timestamps across 9 artifacts, payload and envelope
metadata, match the canonical form. The one remaining dash-separated value is
metadata.run_id, which is a path-safe identifier naming the run directory (':'
is illegal in Windows paths), not a timestamp field — collected_at beside it
carries the same instant canonically.
Adds tests/test_ver_timestamps.py, the first fetcher-level test in the repo. It
loads ver_common by path, since fetchers are scripts the runner exec's rather
than an importable package.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…egory A four-angle quality pass (reuse, simplification, efficiency, altitude) over the branch diff. Net -87 lines, one real defect, and one security gap. Defect — `programs target` could write Paramify UUIDs into GitLab targets: program_target_fetchers selected any fanout fetcher declaring a `project_id` target field. gitlab_ci_cd_pipeline_config, gitlab_project_summary and gitlab_merge_request_summary all declare exactly that, so in a manifest holding both categories, `programs target` with no fetcher argument targeted them with Paramify program UUIDs. validate() doesn't check target-field completeness, so it surfaced at run time. Now scoped by category as well — the honest discriminator while this command group is Paramify-specific. A target_schema field declaring what it identifies would let it generalize; that's a follow-up. add_program_targets likewise no longer writes a `program_name` the targeted fetcher's schema doesn't declare. Security — list_programs sent a Bearer token to whatever PARAMIFY_API_BASE_URL named, including plaintext http. It now enforces the same rule the evidence uploader already does (uploader._base_url_error): https only, localhost exempt. Consistency — categories_needing_config decided "is it set" by truthiness while effective_config, added in the same PR, decided it by membership. A required field set to "" read as set in the TUI and missing in the CLI. Both are now views over effective_config, so there is one definition. Efficiency: - `paramify programs target` ran 4 discover_fetchers + 3 discover_platforms for one immutable result — each walks and jsonschema-validates all 125 fetcher.yaml (~165 ms). Now 1 + 1: measured 578 ms -> 152 ms. Both api helpers already took optional pre-discovered maps; the CLI just wasn't passing them. - The TUI's manifest redraw called validate() and effective_config() with no shared discovery, doubling the blocking work on every mutation and tab switch (272 ms -> ~166 ms). Adds api.discover() for the one-pass-and-thread pattern. Simplification: - _config_field_def + categories_declaring_config + categories_needing_config (61 lines, a third copy of the platform<-entry merge) -> one categories_for_config filtering effective_config's output (26). - render._config_rows' `view is None` fallback was unreachable — its only caller always passes a view — and re-implemented that merge minus the platform layer, so it could only ever render the wrong answer this PR set out to fix. - Three copies of the accepted-deviation predicate -> _accepted_deviations(); accepted_deviation's build/sort/index-0 -> max(..., default=None). - build_vdt_summary and build_mrh_summary counted identically and re-typed the disposition labels as literals while DISPOSITION_* constants sat above them — renaming one would have left both summaries reporting zeros. Now _detail_counts, keyed off the constants. - report_period_bounds' inner closure took a flag whose first branch was dead on the one call that passed False. - _parse_selection's two numeric branches are one: "4" is the range "4-4". - _SHARED_CONFIG_PROMPTS needed a second dict just to map its names back to the CLI parameters; inlined. list_programs' three never-passed parameters dropped. - 13 test invocations of the same 5-line argv -> a _target() helper, so each test shows only what it varies. Behaviour re-verified end to end against the stub after every change: 9 invocations across 3 programs x 3 fetchers, all exit 0, 42 payload timestamps all canonical, reportPeriod and _summary unchanged. 292 tests, ruff and mypy clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Need to fix bug where paramify program target only allows uri and date entry on unset. It should prompt every time |
Five key/focus bugs, all reachable in normal use, plus the pin that makes the behaviour they depend on a stated requirement rather than a coincidence. - Pressing the number of the tab you are already on killed every page shortcut. `_go_to_tab` clears focus and then assigns `TabbedContent.active`; assigning the value it already holds fires no `TabActivated`, so nothing re-homed focus, and a page's bindings only resolve while focus is inside that page. `a`/`e`/ `x`, `ctrl+r`, `j`/`k` and the arrows all went dead until you pressed escape or a different tab. Re-home focus directly in that case. - `ctrl+p` on the Paramify tab opened Textual's command palette instead of Preview — the palette claims it as a *priority* binding, checked ahead of the focused widget, so the page binding could never fire. We register no command providers, so the palette only offers Textual's own built-ins: turn it off and keep the key. `p` now works too, mirroring the Manifest tab. - `enter` did nothing on the two tables whose footer said it did something: on a run it now drills into that run's evidence files (where enter opens one), and on a manifest row it opens the entry editor. - Editing the manifest's output dir lost the path. Textual selects an `Input`'s value on focus, so the first keystroke replaced it wholesale; and an edit not submitted with `enter` was silently reverted by the next `rebuild()`. Focus no longer selects, and blur commits. - `enter` in a confirmation dialog meant Yes, because Yes is composed first and took the default `AUTO_FOCUS` — on the dialogs that delete a manifest file, remove an entry, and upload to Paramify. Focus No; `y` still confirms. The footer now lists `esc` (the only way out of a focused text field back to the shortcut keys — an `Input` consumes every printable key) and the Run tab shows `enter/ctrl+r`, since focus opens on the ▶ Run button and Button binds enter. Advertised rather than rebound: the status table can't hold focus before the first run, so moving focus there would leave `ctrl+r` dead instead. The `tui` extra pins `textual>=8,<9` (was `>=1.0,<2.0`, which nobody ran). `select_on_focus` and the focus semantics above differ enough that the TUI is not the same app on 1.x. `tests/test_tui_keys.py` drives the real app through Textual's pilot to hold the contract: what each tab focuses, that the globals survive a repeat tab press, and that enter reaches an action wherever the footer says it does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--cert-uri` and `--report-from` were prompted for only when genuinely missing, so a manifest that already had them never showed them again. That made the two values a black box: the only way to see what the next run would stamp into every report — or to correct a wrong URI, or roll the report window forward — was to open the manifest and edit `platforms.paramify.config` by hand. Adding a program and moving the window are the same routine, and the command only served one. Both are now shown on every interactive run, with the value in force as the prompt default and a line saying where it comes from (`platforms.paramify`, an entry's own config, or not set yet) — provenance the bracketed default can't carry, and it matters because an entry-level override outranks the category value this command writes. Enter keeps the value and writes nothing: an unchanged answer already in force everywhere skips the write, so a run that just adds a program leaves the platform block byte-identical. Typing over it updates the category value. Entries that resolve to *different* values get no default, only a note that they differ: offering one entry's answer as the manifest's would misreport the others. The ISO check on `report_from` now guards the typed answer as well as the flag, and a rejected one exits before anything is written. `--json` and a piped stdin are unchanged — nothing prompts, a flag overrides without asking, and only a genuinely missing value is an error. Implementation: `categories_for_config` becomes `shared_config_state`, which answers what a front-end editing a shared field actually needs (the categories that accept it, the value in force, its sources, whether entries disagree, and what's still missing) instead of just a category list. Still one view over `effective_config`, so "is it set" keeps a single definition. Five interactive CLI tests cover the new path; `_can_prompt` is the seam they patch, since CliRunner's stdin is not a tty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on the three Paramify FedRAMP VER report fetchers (VER-RPT-AVI, VER-RPT-VDT, VER-TFR-MRH) already on this branch: fixes what a review of them turned up, corrects where their config is declared, normalizes their timestamps, and adds a way to fan them out across a workspace without anyone copying project UUIDs by hand.
Seven commits, each green on its own (checked out and gated individually, so a bisect can't land on a broken one).
1. Fixes to the VER fetchers (
b799193)Correctness, all in the shared module so the AVI/VDT partition can't drift:
RISK_ADJUSTMENTreportedfinalDisposition: "Partially Mitigated". Mitigation now requires an accepted deviation — claiming partial mitigation on the strength of a decision nobody has made yet is a compliance misstatement.poamIdnoridraisedKeyErrorand killed the whole report.PARAMIFY_HTTP_TIMEOUTwasint()-parsed at import, so a malformed value aborted the run with a bareValueErrorbeforemain()could log anything.report_tosilently over-included up to a day past the declared period.Contract — the fetchers declared
report_from/report_to/api_base_url/http_timeoutundersecrets[]. Every declared secret is mandatory (the runner raises when a manifest omits one), so the optional knobs were effectively required, contradicting their documented defaults. They're config now.cert_package_uri,api_base_urlandhttp_timeoutwent further, to category config — one workspace publishes one certification package URI and talks to one Paramify instance, so these are set once underplatforms.paramify.configrather than copied onto every target. Mirrorsrippling.yaml, which already declaresbase_url/page_sizeat category level. Also declaredPARAMIFY_REPORT_TO, which the fetchers read and the README documented but which was declared nowhere, so the runner could never set it.Evidence payload —
_summarynow carries acollectionblock (status + the API-failure ledger)./issuesis the only call these fetchers make, so a failure leaves the report arrays empty; the house pattern (20+ fetchers) is to still write the file with the ledger inside it, and without this an empty failed report is indistinguishable from a genuinely clean one to anything reading the payload. The uploader'sskip_faileddefaults to false, so that artifact does get uploaded.2.
paramify programs(23db1e7)The API identifies programs by UUID; the UI and the people running this use names.
targetselects interactively (numbers, ranges,all), by--program NAME|ID, or--all; resolves a selector as an exact id, then an exact name, then a unique case-insensitive substring, and refuses an ambiguous match rather than guessing. With no fetcher argument it targets every Paramify-program entry in the manifest, so one command fans out all three reports. Re-running tops the manifest up instead of duplicating targets.Shared values that don't vary per program (
--cert-uri,--report-from) are written toplatforms.<category>.configrather than onto every target, and are shown and editable on every interactive run — see §7, which reworked this.--report-fromis validated as an ISO date up front: an unparseable one yields an empty report window, which silently drops every closed issue rather than failing.Targets also carry
program_name, so evidence files and uploaded artifacts read as… - Alpha Cloud Servicesinstead of a bare UUID (a UUID prefix stays in the filename — program names aren't guaranteed unique, and a collision would make the runner's dir-diff output discovery report the second invocation as having produced nothing).The workspace lookup (
GET /projects) is ~50 lines ofrequestsin the api facade rather than a dependency onparamify-sdk, whichpfyuses: the SDK is pinned by a private git URL and this repo is heading for public release, so a git dependency would breakpip installfor outside users. Selection, resolution and manifest wiring live inframework.apiso the TUI can grow the same capability without reimplementing it;targetcomposesapi.add_target(), so it produces exactly the manifest a hand-writtenmanifest add-targetwould.3. TUI: config inherited from the category showed as unset (
f38f6fb)The manifest screen read only a fetcher entry's own
configblock, so a value set once at the category level rendered asrequired — unseton every entry inheriting it, and the summary column undercounted.api.validate()got this right all along — which is why the CLI called a manifest runnable while the TUI showed a missing required field.Pre-existing: it would have misreported rippling's
base_url/page_sizeand checkov'ssoft_failthe same way. Moving the VER fetchers' shared knobs to category config is just what made it reachable.Fixed with
api.effective_config(), which performs the same merge the runner does (platform defaults ← platform values ← per-fetcher values) and returns each field with its value plus the layer it came from. Both the detail pane and the count render that. Category-declared fields now appear at all —cert_package_uriwas previously invisible on entries it applies to, despite being required and injected into every invocation.4. One timestamp format everywhere (
5fc52b0)A generated report mixed three notations, because it drew on three sources and only one was formatted: fetcher-generated values (already canonical), values passed through verbatim from the API (millisecond precision), and report bounds echoed from config (a bare date). All three now go through
to_utc_z():Non-UTC offsets are converted rather than preserved. Unparseable input passes through unchanged — losing a value the source gave us is worse than an off-format one, and schema verification is where that should surface. The bounds are normalized for display only; the raw config values still drive the coverage window, since normalizing first would turn a date-only bound into a midnight instant and drop that day's closures. For the same reason a date-only
report_toreports as that day's last second.Adds
tests/test_ver_timestamps.py, the first fetcher-level test in the repo (loadsver_commonby path, since fetchers are scripts the runner exec's rather than an importable package).5. Simplification pass (
c3ebf96) — net −87 lines, and two defectsA four-angle quality review (reuse / simplification / efficiency / altitude) over the branch diff. Both defects were introduced and fixed within this branch, so the diff against main shows neither — flagging them here so they get review attention:
programs targetcould write Paramify UUIDs into GitLab targets.program_target_fetchersselected any fanout fetcher declaring aproject_idtarget field, and all three GitLab fetchers declare exactly that.validate()doesn't check target-field completeness, so it surfaced at run time. Now scoped by category too;add_program_targetsalso no longer writes aprogram_namethe target schema doesn't declare.list_programssent a Bearer token to whateverPARAMIFY_API_BASE_URLnamed, including plain http. Now enforces the rule the evidence uploader already had — https only, localhost exempt.Also fixed a latent inconsistency:
categories_needing_configdecided "is it set" by truthiness whileeffective_config, added in the same PR, decided it by membership — so a required field set to""read as set in the TUI and missing in the CLI. Both are now views overeffective_config.Efficiency:
programs targetran 4discover_fetchers+ 3discover_platformsfor one immutable result (each walks and jsonschema-validates all 125fetcher.yaml, ~165 ms) — now 1 + 1, 578 ms → 152 ms. The TUI's manifest redraw calledvalidate()andeffective_config()with no shared discovery, doubling blocking work on every mutation and tab switch — 272 ms → ~166 ms.Simplification highlights: three config helpers (61 lines, a third copy of the platform←entry merge) → one 26-line filter;
render._config_rows' fallback was unreachable and re-implemented that merge minus the platform layer, so it could only ever render the wrong answer this PR set out to fix; three copies of the accepted-deviation predicate → one; the VDT/MRH summaries re-typed the disposition labels as string literals while theDISPOSITION_*constants sat above them, so renaming one would have left both silently reporting zeros.6. TUI: five dead keys, and the textual line it's actually tested against (
dd846fd)Each of these is reachable in normal use:
_go_to_tabclears focus and then assignsTabbedContent.active; assigning the value it already holds fires noTabActivated, so nothing re-homed focus — and a page's bindings only resolve while focus is inside that page.a/e/x,ctrl+r,j/kand the arrows all went dead until you pressed escape or a different tab.ctrl+pon the Paramify tab opened Textual's command palette instead of Preview. The palette claims that key as a priority binding, checked ahead of the focused widget, so the page's own binding could never fire. We register no command providers (the palette only offers Textual's built-ins), so it's off.pnow works too, mirroring the Manifest tab.enterdid nothing on the two tables whose footer said it did something. On a run it now drills into that run's evidence files (where enter opens one); on a manifest row it opens the entry editor.Input's value on focus, so the first keystroke replaced it wholesale; and an edit not submitted withenterwas silently reverted by the nextrebuild(). Focus no longer selects, and blur commits.enterin a confirmation dialog meant Yes — Yes is composed first and took the defaultAUTO_FOCUS, on the dialogs that delete a manifest file, remove an entry, and upload to Paramify. Focus No;ystill confirms.The footer now lists
esc(the only way out of a focused text field back to the shortcut keys — anInputconsumes every printable key) and the Run tab showsenter/ctrl+r. That last one is advertised rather than rebound: the status table can't hold focus before the first run, so moving focus there would leavectrl+rdead instead.The
tuiextra pinstextual>=8,<9(was>=1.0,<2.0, which nobody ran —select_on_focusand the focus semantics above differ enough that the TUI is not the same app on 1.x).tests/test_tui_keys.pydrives the real app through Textual's pilot to hold the contract: what each tab focuses, that the globals survive a repeat tab press, and that enter reaches an action wherever the footer says it does.7.
programs target's shared config is shown and editable every run (2d8a634)As shipped in §2,
--cert-uriand--report-fromwere prompted for only when genuinely missing, so a manifest that already had them never showed them again. That made the two values a black box: the only way to see what the next run would stamp into every report — or to fix a wrong URI, or roll the report window forward — was to open the manifest and editplatforms.paramify.configby hand. Adding a program and moving the window are the same routine, and the command served only one.The provenance line is not decoration: an entry's own
configoutranks the category value this command writes, so "where is this coming from" changes what your answer will and won't affect — and the bracketed default can't say it. Enter keeps the value and writes nothing (an unchanged answer already in force everywhere skips the write, so a run that just adds a program leaves the platform block byte-identical); typing over it updates the category value.Entries that resolve to different values get no default, only a note that they differ — offering one entry's answer as the manifest's would misreport the others. The ISO check on
report_fromnow guards the typed answer as well as the flag, and a rejected one exits before anything is written.--jsonand a piped stdin are unchanged: nothing prompts, a flag overrides without asking, and only a genuinely missing value is an error.categories_for_configbecomesshared_config_state, which answers what a front-end editing a shared field actually needs — the categories that accept it, the value in force, its sources, whether entries disagree, and what's still missing — instead of a bare category list. Still one view overeffective_config(), so "is it set" keeps a single definition (the drift §5 fixed).Verification
ruff check framework/,mypy framework/,pytest(307 pass, 70 new), and the CLI discovery smoke over all 125 fetchers — green on the tip and on each commit individually.Behaviour exercised end to end against a stubbed workspace serving
/projectsand/issues(no live tenant):programs list→programs target→validate→runproduced 9 invocations across 3 programs × 3 fetchers, all exit 0, each program's file distinct, one evidence set per report, the shared cert URI in all 9 payloads, and all 42 payload timestamps canonical. The failure path too: a dropped/issuescall exits 1 and recordscollection.status: "failed"with the exception in the payload. Interactive prompts were driven through a real pty to confirm each program's values land on the right target.§6 and §7 were verified on their own terms, not by re-running that stub: the TUI keys through Textual's pilot (
tests/test_tui_keys.py), and the reworked prompting through five CLI tests plus a scratch run over the four flows in sequence — first run with nothing set, enter keeping both, typing a new date, and a bad date rejected with the manifest left untouched.Known gaps — need a live tenant, not a code change
One real run answers all three:
/issuesis not paginated. Acknowledged in the code. If the endpoint caps a page, every report is silently short.paramify-sdkpaginates nothing anywhere either — corroboration, not proof.{OPEN, CLOSED}. Any third status whosestatusDatefalls outside the report window is dropped entirely; inside it, it gets nofinalDispositionandisOverdue: falseregardless ofdueDate.LEVEL_TO_NRATINGcontains"CHILL", which appears nowhere else in the repo. Matching is exact and case-sensitive; an unknown level silently omitscurrentRating.Deferred follow-ups
Identified in the review, deliberately not in this PR:
executor._apply_config,api.validate,api.effective_config) and has already drifted — the executor filtersNone, the others don't. Extracting it touches the runner.type: datein the config schema, soreport_fromis validated only on the CLI flag path — not viamanifest set-platform-config, the TUI, or a hand-edited manifest._TITLE_KEYSpreference list is standing in for a fetcher declaring which target field is its display identity.schema_binding/ vendored FedRAMP schema yet (pairs withfeat/schema-verification).nowfor the 192-day acceptance clock and overdue status rather thanreport_to, so re-running for a past period doesn't reconstruct that period's state.🤖 Generated with Claude Code